using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using Red.Core;
using Red.Core.Containers;
using Red.Core.Extensions;

namespace Red.Controls.ViewModels
{
    /// <summary>
    ///     Type of contained value.
    /// </summary>
    public enum DataItemKind
    {
        Simple, Compound, Container, Hidden, Dummy, Unknown
    }

    /// <summary>
    ///     Base ViewModel for hierarchical data taken from the external source (like Interop). 
    ///     Provides means for asynchronous children enumeration.
    /// </summary>
    public abstract class DataItem : ViewModel
    {
        #region Debug Utils

#if DEBUG
        private readonly Guid _instanceId = Guid.NewGuid();

        public Guid InstanceId
        {
            get { return _instanceId; }
        }
#endif

        #endregion Debug Utils

        #region Data

        //  hierarchical data
        private readonly DataItem _parent;
        private readonly ObservableCollectionExt<DataItem> _children = new ObservableCollectionExt<DataItem>();
        private readonly List<DataItem> _unlistedChildren = new List<DataItem>();
        private AsyncLock _enumerateChildrenLock = new AsyncLock();
        private ICollectionView _collectionView;

        // item state
        private bool _isReadOnly = false;
        private bool _isExpanded = false;
        private bool _isSelected = false;
        private bool _isEnumerated = false;
        private bool _isFetched = false;
        private bool _isDropTarget = false;
        private bool _isActive = false;
        private bool _isValid = true;
        private string _lastErrorMessage = string.Empty;

        // behaviours
        private bool _canBeActivated = false;

        // item data
        private readonly static List<CommandWrapper> _emptyCommands = new List<CommandWrapper>();
        private readonly static List<RedMenuDataItem> _emptyMenu = new List<RedMenuDataItem>();
        private readonly static char _defaultPathSeparator = '\\';

        private string _title = string.Empty;
        private string _toolTip = string.Empty;
        private Canvas _image;

        // filtering children
        private Predicate<object> _filter;

        #endregion Data

        #region C-tor

        public DataItem( DataItem parent, string errorMessage = "", bool preFetched = false )
        {
            _parent = parent;
            _isFetched = preFetched;

            if ( !string.IsNullOrEmpty( errorMessage ) )
                HandleDataError( errorMessage );

            _collectionView = CollectionViewSource.GetDefaultView( _children );
            _children.CollectionChanged += _children_MaintainFilter;
        }

        #endregion C-tor

        #region Properties

        public DataItem Parent
        {
            get { return _parent; }
        }

        /// <summary>
        /// Returns the logical child items of this object.
        /// </summary>
        public ObservableCollection<DataItem> Children
        {
            get { return _children; }
        }

        public ICollectionView ChildrenView
        {
            get { return _collectionView; }
        }

        #region State properites

        public bool IsValid
        {
            get { return _isValid; }
            protected set
            {
                if ( value != _isValid )
                {
                    _isValid = value;
                    OnPropertyChanged();
                }
            }
        }

        public string LastErrorMessage
        {
            get { return _lastErrorMessage; }
            protected set
            {
                if ( value != _lastErrorMessage )
                {
                    _lastErrorMessage = value;
                    OnPropertyChanged();
                }
            }
        }

        public virtual bool IsVisible { get { return true; } }

        public bool IsReadOnly
        {
            get { return _isReadOnly; }
            set
            {
                if ( value != _isReadOnly )
                {
                    _isReadOnly = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Gets/sets whether the container item
        /// associated with this object is expanded.
        /// </summary>
        public bool IsExpanded
        {
            get { return _isExpanded; }

            set
            {
                if ( value != _isExpanded )
                {
                    _isExpanded = value;
                    OnPropertyChanged();
                    if ( value )
                    {
                        BuildAsyncInternal( false, false, false ).DoNotAwait();
                    }
                }
            }
        }

        /// <summary>
        /// Gets/sets whether the container item 
        /// associated with this object is selected.
        /// </summary>
        public bool IsSelected
        {
            get { return _isSelected; }

            set
            {
                if ( value != _isSelected )
                {
                    _isSelected = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        ///     Item can be activated by double clicking on it with LMB when property CanBeActivated is set to 'True'
        /// </summary>
        public bool IsActive
        {
            get { return _isActive; }
            set
            {
                if ( !CanBeActivated )
                    return;

                if ( value != _isActive )
                {
                    _isActive = value;
                    OnPropertyChanged( "IsActive" );
                }
            }
        }

        /// <summary>
        ///     Is the item a drop target, showing a 'you can drop here' visual clue. Handled internally.
        /// </summary>
        public bool IsDropTarget
        {
            get { return _isDropTarget; }

            set
            {
                if ( value != _isDropTarget )
                {
                    _isDropTarget = value;
                    OnPropertyChanged();
                }
            }
        }
        #endregion State Properties

        #region Behaviours properties

        /// <summary>
        ///     Indicates whether this item can be activated by double clicking
        /// </summary>
        public bool CanBeActivated
        {
            get { return _canBeActivated; }
            set
            {
                if ( value != _canBeActivated )
                {
                    _canBeActivated = value;
                    OnPropertyChanged();
                }
            }
        }

        public bool RebuildOnChange { get; set; }

        public virtual IEnumerable<CommandWrapper> Commands
        {
            get { return _emptyCommands.AsReadOnly(); }
        }

        public virtual IEnumerable<RedMenuDataItem> Menu
        {
            get { return _emptyMenu.AsReadOnly(); }
        }

        #endregion

        #region Data properties

        /// <summary>
        ///     Embedded data type name. This is used as a key to search an appropriate DataTemplate.
        /// </summary>
        public abstract string Type { get; }

        public abstract string Value { get; set; }

        public abstract DataItemKind Kind { get; }

        public virtual string Group { get { return string.Empty; } }

        /// <summary>
        ///     Text of the item
        /// </summary>
        public string Title
        {
            get { return _title; }

            set
            {
                if ( value != _title )
                {
                    _title = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        ///     Tooltip shown when mouse hovers over the item
        /// </summary>
        public string ToolTip
        {
            get { return GetTooltip(); }

            set
            {
                if ( value != _toolTip )
                {
                    _toolTip = value;
                    OnPropertyChanged();
                }
            }
        }

        /// <summary>
        ///     Icon displayed next to the item
        /// </summary>
        public Canvas Image
        {
            get { return _image; }

            set
            {
                if ( value != _image )
                {
                    _image = value;
                    OnPropertyChanged();
                }
            }
        }

        #endregion

        #region Filter Properties

        /// <summary>
        /// Recursively set a filter on this DataItem and all of it's children
        /// </summary>
        /// <remarks>
        /// When you set a filter on a <see cref="DataItem"/>, that filter is also applied to all descendants of that <see cref="DataItem"/>
        /// Evaluation of a filter is carried out on all descendants, and is considered to be a match for this <see cref="DataItem"/> if any of it's descendants is a match
        /// </remarks>
        public Predicate<object> Filter
        {
            get { return _filter; }
            set
            {
                if ( value != null && ChildrenView.Filter == null )
                {
                    ChildrenView.Filter = new Predicate<object>( FilterInternal );
                }
                else if ( value == null )
                {
                    ChildrenView.Filter = null;
                }

                _filter = value;

                foreach ( DataItem child in Children )
                {
                    child.Filter = value;
                }
            }
        }

        #endregion Filter Properties

        /// <summary>
        ///     How this element appear in a path. Defaults to Title, but descendant class may override this.
        ///     E.g. if you want an element to say 'Root', but it's path element to be in fact 'd:\blah\root"
        /// </summary>
        public virtual string PathElement
        {
            get { return Title; }
        }

        /// <summary>
        ///     The path identifying this element, consisting of parent's Path and this item's PathElement combined with CombinePath method.
        ///     The path is build on demand to save memory, but descendant classes may override this if the more efficient 
        ///     implementation is available (because, for example, the whole path for the element is stored anyway).
        /// </summary>
        public virtual string Path
        {
            get
            {
                return Parent != null
                    ? CombinePath( Parent.Path, PathElement )
                    : PathElement;
            }
        }

        /// <summary>
        ///     "True" means that even if the item has no children, it can be expanded to populate them "on demand". By default true for Compounds 
        ///     and Containers, but you can override it, mostly to return false in cases, when you know it won't have children anyway, so the "expander" 
        ///     in the UI won't be shown.
        /// </summary>
        public virtual bool IsExpandable
        {
            get { return Kind == DataItemKind.Compound || Kind == DataItemKind.Container; }
        }

        #endregion Properties

        #region Public methods

        /// <summary>
        ///     Populates item's children (only when they weren't populated before).
        /// </summary>
        public async Task BuildAsync( bool recursive = false )
        {
            await BuildAsyncInternal( recursive, false, false );
        }

        /// <summary>
        ///     Clears and re-populates item's children.
        /// </summary>
        public async Task RebuildAsync( bool recursive = false, bool forceFetch = true )
        {
            await BuildAsyncInternal( recursive, true, forceFetch );
        }

        /// <summary>
        ///     Refreshes the item state (value, validity, etc)
        /// </summary>
        public async Task RefreshAsync( bool recursive = true )
        {
            await FetchAsync( true );

            if ( recursive )
                foreach ( DataItem child in _children )
                    await child.RefreshAsync( true );
        }

        #region Drag and Drop support

        /// <summary>
        ///     Returns data that is then dragged as a part of drag-and-drop operation
        /// </summary>
        /// <returns>
        ///     Dragged data, if the item is meant to be draggable, or null (default)
        /// </returns>
        public virtual IDataObject GetDraggedData()
        {
            return null;
        }

        /// <summary>
        ///     Allows the derived class to decide, if the dragged data is accepted by the element
        /// </summary>
        /// <param name="obj">
        ///     Object that is about to be dropped
        /// </param>
        /// <returns>
        ///     Returns true if the item accepts the data to be dropped
        /// </returns>
        public virtual bool AcceptsDraggedData( IDataObject obj )
        {
            return false;
        }

        /// <summary>
        ///     Allows the derived class to define an action after a dragged data is dropped onto this item
        /// </summary>
        /// <param name="obj">
        ///     Object that was dropped
        /// </param>
        public virtual void DropDraggedData( IDataObject obj )
        {
            // empty by design
        }

        #endregion Interface

        /// <summary>
        ///     Expands hierarchy all the way down to access an item on a given relative path. If the path doesn't exist, 
        ///     it will fall-back to ProvideUnlistedChild, which by defaults returns null, but can be overridden by the descendant class.
        /// </summary>
        /// <param name="path">
        ///     Path to a sub-view - a series of item's names separated by a dot. You can also use the special keyword $parent
        ///     instead of an item's name to go up the hierarchy (it's like double dot in a directory path).
        /// </param>
        public async Task<DataItem> DigUpSubItemAsync( string path )
        {
            string theRest;
            string firstPathSegment = SplitPath( path, out theRest );

            if ( firstPathSegment == "$root" )
            {
                DataItem cur = this;
                while ( cur.Parent != null )
                    cur = cur.Parent; // go to the root
                return await cur.DigUpSubItemAsync( theRest );
            }
            else if ( firstPathSegment == "$parent" )
            {
                if ( Parent != null )
                    return await Parent.DigUpSubItemAsync( theRest ); // go up the hierarchy
                else
                    throw new InvalidOperationException( "Cannot use $parent syntax on a root item" );
            }
            else
            {
                using ( await _enumerateChildrenLock.LockAsync() )
                {
                    await EnumerateAsync( false ); // ensure has the children list is enumerated
                    return await GetSubItemAsync( firstPathSegment, theRest );
                }
            }
        }

        /// <summary>
        ///     Expands hierarchy all the way down to access an item and returns its value
        /// </summary>
        /// <param name="path">
        ///     Path to sub-view containing value <see cref="DigUpSubItemAsync"/>
        /// </param>
        /// <param name="def">
        ///     Value to return when either the sub-item doesn't exist or is not convertible to T
        /// </param>
        public async Task<T> GetSubValueAsync<T>( string path, T def = default( T ) ) where T : IConvertible
        {
            DataItem subItem = await DigUpSubItemAsync( path );
            if ( subItem != null )
            {
                var converter = TypeDescriptor.GetConverter( typeof( T ) );
                if ( converter.IsValid( subItem.Value ) )
                    return (T)converter.ConvertFrom( subItem.Value );
                else
                    return def;
            }
            else
                return def;
        }

        /// <summary>
        ///     Expands hierarchy all the way down to access an item and sets it value
        /// </summary>
        /// <param name="path">
        ///     Path to sub-view containing value <see cref="DigUpSubItemAsync"/>
        /// </param>
        /// <param name="value">
        ///     Value to set
        /// </param>
        public async Task SetSubValueAsync<T>( string path, T value ) where T : IConvertible
        {
            DataItem subItem = await DigUpSubItemAsync( path );
            if ( subItem != null )
            {
                subItem.Value = value.ToString();
            }
        }

        /// <summary>
        ///     Finds an descendant item by the given path, looking up by items' PathElement property
        /// </summary>
        /// <remarks>
        ///     Because the tree evaluation is lazy. in order to be able to find an item on a particular path, 
        ///     the method has to expand the whole tree path leading to the item.
        /// </remarks>
        /// <param name="path">
        ///     Sub-path to the item, with elements separated by System.IO.Path.DirectorySeparatorChar
        /// </param>
        /// <returns>
        ///     Found element or null
        /// </returns>
        public DataItem FindDescendantByPath( string path )
        {
            if ( PathElement.Length == 0 || path.StartsWith( PathElement ) )
            {
                string rest = path.Substring( PathElement.Length );
                if ( rest.StartsWith( System.IO.Path.DirectorySeparatorChar.ToString() ) )
                {
                    rest = rest.Substring( 1 );
                }

                if ( rest.Length == 0 )
                {
                    return this;
                }
                else
                {
                    IsExpanded = true;
                    foreach ( var child in Children )
                    {

                        var subItem = child.FindDescendantByPath( rest );
                        if ( subItem != null )
                        {
                            return subItem;
                        }
                    }
                }
            }

            return null;
        }

        /// <summary>
        ///     Executes (asynchronously) callable action
        /// </summary>
        public void Execute( string funcName, params object[] callArgs )
        {
            ExecuteAsyncInternal( funcName, callArgs ).DoNotAwait();
        }

        /// <summary>
        ///     Executes callable action - awaitable version
        /// </summary>
        public async Task<Result> ExecuteAsync( string funcName, params object[] callArgs )
        {
            Result<string> res = await ExecuteAsyncInternal( funcName, callArgs );
            if ( res )
                return Result.Ok;
            else
                return Result.Failed( res.Message );
        }

        /// <summary>
        ///     Executes callable action and returns it's result
        /// </summary>
        public async Task<Result<T>> ExecuteAsync<T>( string funcName, params object[] callArgs ) where T : IConvertible
        {
            Result<string> res = await ExecuteAsyncInternal( funcName, callArgs );
            if ( res )
                return Result<T>.Ok( (T)Convert.ChangeType( res.Value, typeof( T ) ) );
            else
                return Result<T>.Failed( res.Message );

        }

        /// <summary>
        ///     Registers a notifier
        /// </summary>
        public virtual Task<bool> RegisterNotifierAsync( string name, Action<DataItem, string> onNotification )
        {
            return Task.FromResult( false );
        }

        /// <summary>
        /// Run through the tree and re-evaluate the filtering at each level
        /// </summary>
        public void RefreshFilter()
        {
            foreach ( DataItem child in Children )
            {
                child.RefreshFilter();
            }

            ChildrenView.Refresh();
        }

        #endregion Public methods

        #region Event handling methods

        /// <summary>
        /// DataItem was "executed" by double click
        /// </summary>
        public virtual void OnSelectDoubleClick() { }

        /// <summary>
        /// DataItem was selected
        /// </summary>
        public virtual void OnSelect() { }
        

        private void _children_MaintainFilter( object sender, NotifyCollectionChangedEventArgs e )
        {
            if( e.Action == NotifyCollectionChangedAction.Add )
            {
                foreach( DataItem item in e.NewItems )
                {
                    item.Filter = Filter;
                }
            }
        }

        #endregion

        #region Protected methods

        /// <summary>
        ///     Invoked when the item needs to update it's children list (like expanding or after calling Refresh).
        /// </summary>
        /// <param name="children">
        ///     Current list of children to be modified to reflect the current item's state.
        /// </param>
        protected virtual Task EnumerateChildrenAsync( IRangedCollection<DataItem> children )
        {
            return Task.FromResult( true );
        }

        /// <summary>
        ///     Allows the derived object to provide a child which is not on the children list.
        ///     It may serve, for example, as an "error indicator item" to be displayed when asked for non-existing child.
        /// </summary>
        protected virtual Task<DataItem> ProvideUnlistedChild( string path )
        {
            return Task.FromResult<DataItem>( null );
        }

        /// <summary>
        ///     Split up a path passed to DigUpSubItem into the first segment (direct child name) and the rest.
        ///     You can override this method to provide more advanced path syntax.
        /// </summary>
        protected virtual string SplitPath( string path, out string theRest )
        {
            int idx = path.IndexOf( _defaultPathSeparator );
            string result = idx == -1 ? path : path.Substring( 0, idx );
            theRest = idx == -1 ? string.Empty : path.Substring( path[idx] == _defaultPathSeparator ? idx + 1 : idx );
            return result;
        }

        /// <summary>
        ///     Combines two path parts into one.
        ///     You can override this method to provide more advanced path syntax.
        /// </summary>
        protected virtual string CombinePath( string pathA, string pathB )
        {
            if ( string.IsNullOrEmpty( pathA ) )
                return pathB;
            else if ( string.IsNullOrEmpty( pathB ) )
                return pathA;
            else
                return pathA.TrimEnd( _defaultPathSeparator ) + _defaultPathSeparator + pathB.TrimStart( _defaultPathSeparator );
        }

        /// <summary>
        ///     Executes callable action and returns it's result - overridable implementation
        /// </summary>
        protected virtual Task<Result<string>> ExecuteAsyncInternal( string funcName, params object[] callArgs )
        {
            return Task.FromResult( Result<string>.Failed( "Not implemented" ) );
        }

        /// <summary>
        ///     Allows the external data source to synchronize itself (fetch the current data). 
        ///     May be also used for data validation.
        /// </summary>
        protected virtual Task<Result> FetchAndValidateAsync()
        {
            return Task.FromResult( Result.Ok );
        }

        protected void HandleDataOk()
        {
            IsValid = true;
        }

        protected void HandleDataError( string message )
        {
            IsValid = false;
            LastErrorMessage = message;
        }

        #endregion Protected methods

        #region Private methods

        private async Task BuildAsyncInternal( bool recursive, bool forceEnumerate, bool forceFetch )
        {
            await FetchAsync( forceFetch );

            using ( await _enumerateChildrenLock.LockAsync() )
            {
                await EnumerateAsync( forceEnumerate );
                // lock has to wrap up also this as _children might get modified during enumeration by re-entrant call above
                foreach ( var child in _children )
                    if ( child.IsExpanded || recursive )
                        await child.BuildAsyncInternal( recursive, forceEnumerate, forceFetch );
            }
        }

        private async Task<DataItem> GetSubItemAsync( string firstPathSegment, string theRest )
        {
            DataItem subItem = _children.FirstOrDefault( child => child.Title == firstPathSegment ) as DataItem;
            if ( subItem == null )
            {
                subItem = _unlistedChildren.FirstOrDefault( child => child.Title == firstPathSegment );
                if ( subItem == null )
                {
                    subItem = await ProvideUnlistedChild( firstPathSegment );
                    if ( subItem == null )
                        return null;

                    _unlistedChildren.Add( subItem );
                }
            }

            if ( string.IsNullOrEmpty( theRest ) )
                return subItem;
            else
                return await subItem.DigUpSubItemAsync( theRest );
        }

        private async Task FetchAsync( bool force )
        {
            if ( !_isFetched || force )
            {
                Result result = await FetchAndValidateAsync();
                OnPropertyChanged( "Value" );
                if ( result )
                {
                    HandleDataOk();
                }
                else
                {
                    HandleDataError( result.Message );
                }

                _isFetched = true;
            }
        }

        private async Task EnumerateAsync( bool force )
        {
            if ( !_isEnumerated || force )
            {
                _unlistedChildren.Clear();

                // Remove possible dummies before enumeration
                _children.RemoveAll( c => c is DummyDataItem );

                await EnumerateChildrenAsync( _children );

                // Give every non-expanded, non-leaf child a dummy, to ensure that an expander is shown in the UI
                foreach ( DataItem child in _children )
                    // NOTE: check if isn't enumerated - EnumerateChildren may not clear children or re-add already enumerated ones (from a pool), etc
                    // If it would be already enumerated, expanding later wouldn't remove the dummy.
                    if ( !child._isEnumerated && child.IsExpandable )
                        child._children.Add( new DummyDataItem( child ) );

                _isEnumerated = true;
            }
        }

        private string GetTooltip()
        {
#if DEBUG
            return $"{_instanceId.ToString()}|{Path}\nType={Type}";
#else
            return _toolTip;
#endif
        }

        private static bool FilterInternal( object obj )
        {
            bool result = false;

            DataItem item = (DataItem)obj;

            result = item.Filter?.Invoke( obj ) ?? true;

            foreach ( DataItem child in item.Children )
            {
                if ( !( child is DummyDataItem ) )
                {
                    result |= FilterInternal( child );
                }
            }

            return result;
        }

        #endregion Private methods
    }

    /// <summary>
    ///     Dummy item created as child for non-leaf, collapsed items
    /// </summary>
    public class DummyDataItem : DataItem
    {
        public override DataItemKind Kind { get { return DataItemKind.Dummy; } }

        public override string Type { get { return ""; } }

        public override string Value { get { return "";  } set {} }
        public DummyDataItem( DataItem parent )
            : base( parent, "" )
        {
        }
    }

}
